#ifndef AMREX_IPARSER_Y_H_
#define AMREX_IPARSER_Y_H_
#include <AMReX_Config.H>

#include <AMReX_GpuQualifiers.H>
#include <AMReX_GpuPrint.H>
#include <AMReX_Math.H>
#include <AMReX_Print.H>

#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <set>
#include <string>
#include <type_traits>

void amrex_iparsererror (char const *s, ...);

namespace amrex {

enum iparser_f1_t {  // Built-in functions with one argument
    IPARSER_ABS = 1
};

enum iparser_f2_t {  // Built-in functions with two arguments
    IPARSER_FLRDIV = 1,
    IPARSER_POW,
    IPARSER_GT,
    IPARSER_LT,
    IPARSER_GEQ,
    IPARSER_LEQ,
    IPARSER_EQ,
    IPARSER_NEQ,
    IPARSER_AND,
    IPARSER_OR,
    IPARSER_MIN,
    IPARSER_MAX
};

enum iparser_f3_t { // functions with three arguments
    IPARSER_IF
};

enum iparser_node_t {
    IPARSER_NUMBER = 1,
    IPARSER_SYMBOL,
    IPARSER_ADD,
    IPARSER_SUB,
    IPARSER_MUL,
    IPARSER_DIV,
    IPARSER_NEG,
    IPARSER_F1,
    IPARSER_F2,
    IPARSER_F3,
    IPARSER_ASSIGN,
    IPARSER_LIST,
    IPARSER_ADD_VP,  /* types below are generated by optimization */
    IPARSER_ADD_PP,
    IPARSER_SUB_VP,
    IPARSER_SUB_PP,
    IPARSER_MUL_VP,
    IPARSER_MUL_PP,
    IPARSER_DIV_VP,
    IPARSER_DIV_PV,
    IPARSER_DIV_PP,
    IPARSER_NEG_P
};

/* In C, the address of the first member of a struct is the same as
 * the address of the struct itself.  Because of this, all struct iparser_*
 * pointers can be passed around as struct iparser_node pointer and enum
 * iparser_node_t type can be safely checked to determine their real type.
 */

union iparser_nvp {
    struct iparser_node* n;
    int v;
    int ip;
};

struct iparser_node {
    enum iparser_node_t type;
    struct iparser_node* l;
    struct iparser_node* r;
    union iparser_nvp lvp; // After optimization, this may store left value/pointer offset.
    int rip;               //                     this may store right      pointer offset.
};

struct iparser_number {
    enum iparser_node_t type;
    int value;
};

struct iparser_symbol {
    enum iparser_node_t type;
    char* name;
    int ip;
};

struct iparser_f1 {  /* Builtin functions with one argument */
    enum iparser_node_t type;
    struct iparser_node* l;
    enum iparser_f1_t ftype;
};

struct iparser_f2 {  /* Builtin functions with two arguments */
    enum iparser_node_t type;
    struct iparser_node* l;
    struct iparser_node* r;
    enum iparser_f2_t ftype;
};

struct iparser_f3 { /* Builtin functions with three arguments */
    enum iparser_node_t type;
    struct iparser_node* n1;
    struct iparser_node* n2;
    struct iparser_node* n3;
    enum iparser_f3_t ftype;
};

struct iparser_assign {
    enum iparser_node_t type;
    struct iparser_symbol* s;
    struct iparser_node* v;
};

static_assert(sizeof(iparser_f3) <= sizeof(iparser_node), "amrex iparser: sizeof iparser_node too small");

/*******************************************************************/

/* These functions are used in bison rules to generate the original AST. */
void iparser_defexpr (struct iparser_node* body);
struct iparser_symbol* iparser_makesymbol (char* name);
struct iparser_node* iparser_newnode (enum iparser_node_t type, struct iparser_node* l,
                                      struct iparser_node* r);
struct iparser_node* iparser_newnumber (int d);
struct iparser_node* iparser_newsymbol (struct iparser_symbol* sym);
struct iparser_node* iparser_newf1 (enum iparser_f1_t ftype, struct iparser_node* l);
struct iparser_node* iparser_newf2 (enum iparser_f2_t ftype, struct iparser_node* l,
                                    struct iparser_node* r);
struct iparser_node* iparser_newf3 (enum iparser_f3_t ftype, struct iparser_node* n1,
                                    struct iparser_node* n2, struct iparser_node* n3);
struct iparser_node* iparser_newassign (struct iparser_symbol* s, struct iparser_node* v);
struct iparser_node* iparser_newlist (struct iparser_node* nl, struct iparser_node* nr);

/*******************************************************************/

/* This is our struct for storing AST in a more packed way.  The whole
 * tree is stored in a contiguous chunk of memory starting from void*
 * p_root with a size of sz_mempool.
 */
struct amrex_iparser {
    void* p_root;
    void* p_free;
    struct iparser_node* ast;
    std::size_t sz_mempool;
};

struct amrex_iparser* amrex_iparser_new ();
void amrex_iparser_delete (struct amrex_iparser* iparser);

struct amrex_iparser* iparser_dup (struct amrex_iparser* source);
struct iparser_node* iparser_ast_dup (struct amrex_iparser* iparser, struct iparser_node* node, int move);

void iparser_regvar (struct amrex_iparser* iparser, char const* name, int i);
void iparser_setconst (struct amrex_iparser* iparser, char const* name, int c);
void iparser_print (struct amrex_iparser* iparser);
std::set<std::string> iparser_get_symbols (struct amrex_iparser* iparser);
int iparser_depth (struct amrex_iparser* iparser);

/* We need to walk the tree in these functions */
void iparser_ast_optimize (struct iparser_node* node);
std::size_t iparser_ast_size (struct iparser_node* node);
void iparser_ast_print (struct iparser_node* node, std::string const& space, AllPrint& printer);
void iparser_ast_regvar (struct iparser_node* node, char const* name, int i);
void iparser_ast_setconst (struct iparser_node* node, char const* name, int c);
void iparser_ast_get_symbols (struct iparser_node* node, std::set<std::string>& symbols,
                              std::set<std::string>& local_symbols);
int iparser_ast_depth (struct iparser_node* node);

/*******************************************************************/

AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE int
iparser_call_f1 (enum iparser_f1_t /*type*/, int a)
{
    /// There is only one type for now
    return std::abs(a);
}

AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE int
iparser_call_f2 (enum iparser_f2_t type, int a, int b)
{
    switch (type) {
    case IPARSER_FLRDIV:
    {
        int r = a/b;
        if (r*b == a || (a < 0 && b < 0) || (a > 0 && b > 0)) {
            return r;
        } else {
            return r-1;
        }
    }
    case IPARSER_POW:
    {
        if (b < 0) {
            return 0;
        } else {
            int r = 1;
            while (b != 0) {
                if (b & 1) {
                    r *= a;
                }
                b >>= 1;
                a *= a;
            }
            return r;
        }
    }
    case IPARSER_GT:
        return (a > b) ? 1 : 0;
    case IPARSER_LT:
        return (a < b) ? 1 : 0;
    case IPARSER_GEQ:
        return (a >= b) ? 1 : 0;
    case IPARSER_LEQ:
        return (a <= b) ? 1 : 0;
    case IPARSER_EQ:
        return (a == b) ? 1 : 0;
    case IPARSER_NEQ:
        return (a != b) ? 1 : 0;
    case IPARSER_AND:
        return ((a != 0) && (b != 0)) ? 1 : 0;
    case IPARSER_OR:
        return ((a != 0) || (b != 0)) ? 1 : 0;
    case IPARSER_MIN:
        return (a < b) ? a : b;
    case IPARSER_MAX:
        return (a > b) ? a : b;
    default:
        amrex::Abort("iparser_call_f2: Unknown function");
        return 0;
    }
}

AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE int
iparser_call_f3 (enum iparser_f3_t /*type*/, int a, int b, int c)
{
    // There is only one type currently
    return (a != 0) ? b : c;
}

}

#endif
